/** api接口文件
 * @file api.cpp
 * @author Kiven Lee
 * @version 1.0
 * @date 2021-08-26
 */
#include "api.hpp"
#include "app.hpp"
#include "klib/log.hpp"
#include "klib/net.hpp"
#include "klib/mjson.hpp"
#include "klib/file.hpp"
#include "klib/uuid.hpp"
#include "api_error_code.hpp"
#include <filesystem>
#include "asio/experimental/deferred.hpp"

const char PARAM_FORMAT_ERROR[] = "参数格式错误";

#define DECLARE_BODY mjson::json_t body(req.body());

#define DECLARE_STRING_VIEW(VAL, CODE) \
    string_view VAL = body.get(#VAL).value<string_view>(""); \
    if (VAL.size() == 0) \
        co_return co_await _failed(req, CODE, PARAM_FORMAT_ERROR);

// jwt发布者
static constexpr char _JWT_ISSUER[] = "accinfo";
// jwt过期时间（分钟为单位）
static constexpr uint32_t _JWT_EXPIRE = 30;
static constexpr char _DATA_DIR[] = "data";

using namespace ai;
using namespace klib;
using namespace klib::http;
using namespace asio;

using jw_t = json::writer_t;

static constexpr string_view _ANONYMOUS_URLS[] = { "/login", "/ping" };
static constexpr string_view _LOCAL_URLS[] = { "/create", "/exit", "/debug", };

static bool _find_urls(const string_view* array, size_t array_size, const string_view& path) {
    for (const string_view* end = array + array_size; array < end; ++array)
        if (server_t::url_match(path, *array))
            return true;
    return false;
}

template<typename... ARGS>
static inline awaitable<void> _failed(request_t& req, ApiCode code, string_view fmt, ARGS&&... args) {
    if (!fmt.size()) fmt = "系统内部错误";
    if (log::is_debug_enabled()) {
        dstring_t s(fmt, std::forward<ARGS>(args)...);
        LOG_DEBUG("api处理失败： code = [{}], message = [{}]", uint32_t(code), s);
    }
    co_await req.error(uint32_t(code), fmt, std::forward<ARGS>(args)...);
}

// 权限校验
static awaitable<bool> _permission_checkd(request_t& req, const string_view& path) {
    const string& sid = req.session_id(true);
    if (!sid.empty() && app::session_manager().exists(sid))
        co_return true;

    LOG_DEBUG("{} 尚未登录，禁止访问 {}", net::ipv4_string_t(req.client_ip()), path);
    co_await req.error(rep_status_t::Unauthorized);
    co_return false;
}

static aidb_t* _get_aidb(request_t& req) {
    auto sess_data = app::session_manager().get_value_ptr(req.session_id(true));
    if (sess_data == nullptr) {
        LOG_DEBUG("用户已过期，需要重新登陆");
        return nullptr;
    }

    auto paidb = sess_data->aidb_ptr.get();
    if (!paidb->is_opened()) {
        path_t dbFile(app::work(), _DATA_DIR, sess_data->user);
        dbFile.append(".").append(AIDB_EXT);

        if (paidb->open(dbFile, sess_data->pass) != aidb_t::opstate::ok) {
            LOG_ERROR("口令错误，无法重新打开数据库[{}]", dbFile);
            return nullptr;
        }
    }

    // 延长数据库存活时间，避免被垃圾回收关闭
    sess_data->aidb_expire_time = std::time(nullptr) + app::db_exp_minutes() * 60;

    return paidb;
}

// 预处理函数，针对sys-api访问，只允许本机访问
awaitable<bool> ai::pre_handler(request_t& req) {
    string_view path = req.path().substr(app::http_server().api_prefix().size());

    if (!_find_urls(_ANONYMOUS_URLS, length_of(_ANONYMOUS_URLS), path)) {
        // 本地接口只允许通过127.0.0.1访问
        if (_find_urls(_LOCAL_URLS, length_of(_LOCAL_URLS), path)) {
            net::ipv4_t cip = req.client_ip();
            if (cip.ip != net::localhost_ipv4.ip) {
                LOG_DEBUG("本地接口[{}]禁止[{}]访问", path, net::ipv4_string_t(req.client_ip()));
                co_await req.error(uint32_t(ApiCode::只允许本地访问), "禁止访问");
                co_return false;
            }
        } else {
            // 普通接口，需要进行登录状态判断
            co_return co_await _permission_checkd(req, path);
        }
    }

    co_return true;
}

// 服务连通性测试ping
awaitable<void> ai::api_ping(request_t& req) {
    // auto s = req.session_id(true);
    // string headers;
    // if (s.empty()) {
    //     s = req.session_id();
    //     LOG_DEBUG("没有读取到session id, 设置新的session id = {}", s);
    //     headers.append(req.make_cookie(req.session_name(), s));
    //     headers.append("\r\n");
    // } else {
    //     LOG_DEBUG("读取的session id = {}", s);
    // }

    static constexpr std::string_view pong("pong");

    mjson::json_t body(req.body());
    auto reply = body.get("reply").value<string_view>(pong);

    co_await req.ok([&req, &reply] (jw_t& w) {
        w.kvs(
            "id", req.id(),
            "reply", reply,
            "now", packtime_t::now(),
            "startup", app::startup_time().to_packtime(),
            "server", app::http_server().name(),
            "clients", app::http_server().conn_count(),
            "uuid", uuid::generate());
    });
}

// 用户登录
awaitable<void> ai::api_login(request_t& req) {
    // 校验参数
    DECLARE_BODY
    DECLARE_STRING_VIEW(user, ApiCode::用户名不能为空)
    DECLARE_STRING_VIEW(pass, ApiCode::密码不能为空)

    // 验证用户名密码
    path_t path(app::work(), _DATA_DIR);
    path_t dbFile(path);
    dbFile.append_path(user).append(".").append(AIDB_EXT);

    using opstate = aidb_t::opstate;
    unique_ptr<aidb_t> aidb_ptr(new aidb_t);
    auto r = aidb_ptr->open(dbFile, pass);
    if (r != opstate::ok) {
        if (r == opstate::password_error)
            co_return co_await _failed(req, ApiCode::用户名或密码错误, "用户名或密码错误");
        else
            co_return co_await _failed(req, ApiCode::打开数据库失败, "打开数据库失败");
    }

    string sid = req.session_id();
    time_t now = std::time(nullptr);

    // 写入session
    app::session_manager().try_emplace(sid, now + app::jwt_exp_minutes() * 60,
            now, user, pass, std::move(aidb_ptr), now + app::db_exp_minutes() * 60);

    // 创建 cookie
    string headers = req.make_cookie(req.session_name(), sid, 24 * 60 * 60, "/");
    headers.append("\r\n").append(http_content_type_json);
    
    co_await req.ok([&req, &sid, now] (jw_t& w) {
        w.kvs(req.session_name(), sid,
            "expire", packtime_t(now + app::jwt_exp_minutes() * 60));
    }, (string_view) headers);
}

// 查询记录
awaitable<void> ai::api_list(request_t& req) {
    auto sess_data = app::session_manager().get_value_ptr(req.session_id(true));
    if (sess_data == nullptr)
        co_return co_await _failed(req, ApiCode::用户过期, "用户已过期，请重新登录");

    // 校验参数
    mjson::json_t body(req.body());
    string_view s = body.get("s").value<string_view>(""); 

    auto paidb = _get_aidb(req);
    if (paidb == nullptr)
        co_return co_await _failed(req, ApiCode::打开数据库失败, "打开数据文件失败");
    
    co_await req.ok([&req, paidb, s] (jw_t& w) {
        w.begin_array("records");
        for (auto iter = paidb->begin(), e = paidb->end(); iter != e; ++iter) {
            auto val = paidb->get(*iter);
            if (val.size && (!s.size()
                    || val.title.find(s) != string_view::npos
                    || val.user.find(s) != string_view::npos
                    || val.url.find(s) != string_view::npos
                    || val.memo.find(s) != string_view::npos)) {
                w.object(string_view(), "id", *iter,
                    "title", val.title,
                    "user", val.user,
                    "pass", val.pass,
                    "url", val.url,
                    "memo", val.memo);
            }
        }
        w.end_array();
    });
}

// 提交记录
awaitable<void> ai::api_post(request_t& req) {
    // 校验参数
    mjson::json_t body(req.body());
    aidb_key_t id = (aidb_key_t) body.get("id").as_int(0);
    aidb_value_t value(
        body.get("title").to_string_view(),
        body.get("user").to_string_view(),
        body.get("pass").to_string_view(),
        body.get("url").to_string_view(),
        body.get("memo").to_string_view());

    if (!value.title.size())
        co_return co_await _failed(req, ApiCode::标题不能为空, "标题不能为空");

    auto paidb = _get_aidb(req);
    if (paidb == nullptr)
        co_return co_await _failed(req, ApiCode::打开数据库失败, "打开数据文件失败");
    id = paidb->put(id, value);
    if (id == aidb_t::nkey)
        co_return co_await _failed(req, ApiCode::写入记录失败, "写入记录失败");

    co_await req.ok("id", id);
}

// 删除记录
awaitable<void> ai::api_del(request_t& req) {
    DECLARE_BODY
    DECLARE_STRING_VIEW(ids, ApiCode::IDS不能为空)

    auto paidb = _get_aidb(req);
    if (paidb == nullptr)
        co_return co_await _failed(req, ApiCode::打开数据库失败, "打开数据文件失败");

    str_split(ids.data(), ids.size(), ',', [paidb] (const char* str, size_t size) {
        char* end;
        uint32_t key = std::strtoul(str, &end, 10);
        if (str < end && key != aidb_t::nkey)
            paidb->erase(key);
    });
    
    co_await req.ok();
}

// 关闭服务器
awaitable<void> ai::api_exit(request_t& req) {
    // 校验参数
    mjson::json_t body(req.body());
    string_view pass = body.get("pass").value<string_view>("");
    
    // 校验关闭口令
    if (app::shutdown_pass().size() && app::shutdown_pass() != pass)
        co_return co_await _failed(req, ApiCode::关闭密码错误, "密码错误");

    app::http_server().close();
    app::io_ctx().stop();

    co_await req.ok(
        "now", packtime_t::now(),
        "server", app::http_server().name(),
        "message", "成功关闭应用程序");
}

// 创建用户数据库
awaitable<void> ai::api_create(request_t& req) {
    // 校验参数
    DECLARE_BODY
    DECLARE_STRING_VIEW(user, ApiCode::用户名不能为空)
    DECLARE_STRING_VIEW(pass, ApiCode::密码不能为空)

    // 校验超级口令
    string_view sys_pass = body.get("sysPass").value<string_view>("");
    if (app::shutdown_pass().size() && app::shutdown_pass() != sys_pass)
        co_return co_await _failed(req, ApiCode::关闭密码错误, "密码错误");

    // 生成数据库
    path_t path(app::work(), _DATA_DIR);
    path_t dbFile(path);
    dbFile.append_path(user).append(".").append(AIDB_EXT);

    aidb_t aidb;
    aidb_t::opstate r = aidb.create(dbFile, pass);
    if (r != aidb_t::opstate::ok) {
        if (!std::filesystem::exists((string) path))
            co_return co_await _failed(req, ApiCode::目录不存在, "创建数据库失败：data目录不存在");
        if (std::filesystem::exists((string) dbFile))
            co_return co_await _failed(req, ApiCode::文件已存在, "创建数据库失败：数据库已存在");
        else
            co_return co_await _failed(req, ApiCode::创建文件失败, "创建数据库失败");
    }

    if constexpr(os_windows) {
        dstring_t fpath(dbFile);
        str_replace_all(fpath.data(), fpath.size(), '\\', '/');
        co_await req.ok("message", fmt::format_string("创建数据库{}成功", fpath));
    } else {
        co_await req.ok("message", fmt::format_string("创建数据库{}成功", dbFile));
    }
}

// 关闭服务器
awaitable<void> ai::api_debug(request_t& req) {
    // 校验参数
    mjson::json_t body(req.body());
    auto pass = body.get("pass").value<string_view>("");
    auto level = body.get("log").as_string_view("debug");
    
    // 校验关闭口令
    if (app::shutdown_pass().size() && app::shutdown_pass() != pass)
        co_return co_await _failed(req, ApiCode::关闭密码错误, "密码错误");

    auto ln = log::get_level_name();
    log::set_level(level);
    auto nn = log::get_level_name();
    if (ln != nn)
        LOG_INFO("日志级别已经动态调整: {} => {}", ln, log::get_level_name());

    co_await req.ok();
}
